﻿using System;
using Mono.Rift.Core.Graphics;
using mrcoreg = Mono.Rift.Core.Graphics;
using OpenTK;
using OpenTK.Graphics.OpenGL;
using Mono.Rift.Core.OpenTK;
using graphics = Mono.Rift.Core.Graphics;
using System.Collections.Generic;
using coretk = Mono.Rift.Core.OpenTK;

namespace Mono.Rift.Platform.OSX
{
	public class OpenTKOSXRenderDevice : RenderDevice
	{
		public GameWindow GameWindow { get; set; }

		private graphics.ShaderFill _defaultFill;

		public LightingParams Lighting { get; set; }

		private static string _directVertexShaderSrc =
			"uniform mat4 View;\n" +
			"attribute vec4 Position;\n" +
			"attribute vec4 Color;\n" +
			"attribute vec2 TexCoord;\n" +
			"attribute vec3 Normal;\n" +
			"varying  vec4 oColor;\n" +
			"varying  vec2 oTexCoord;\n" +
			"varying  vec3 oNormal;\n" +
			"void main()\n" +
			"{\n" +
			"   gl_Position = View * Position;\n" +
			"   oTexCoord = TexCoord;\n" +
			"   oColor = Color;\n" +
			"   oNormal = vec3(View * vec4(Normal,0));\n" +
			"}\n";

		private static string _stdVertexShaderSrc =
			"uniform mat4 Proj;\n" +
			"uniform mat4 View;\n" +
			"attribute vec4 Position;\n" +
			"attribute vec4 Color;\n" +
			"attribute vec2 TexCoord;\n" +
			"attribute vec2 TexCoord1;\n" +
			"attribute vec3 Normal;\n" +
			"varying  vec4 oColor;\n" +
			"varying  vec2 oTexCoord;\n" +
			"varying  vec2 oTexCoord1;\n" +
			"varying  vec3 oNormal;\n" +
			"varying  vec3 oVPos;\n" +
			"void main()\n" +
			"{\n" +
			"   gl_Position = Proj * (View * Position);\n" +
			"   oNormal = vec3(View * vec4(Normal,0));\n" +
			"   oVPos = vec3(View * Position);\n" +
			"   oTexCoord = TexCoord;\n" +
			"   oTexCoord1 = TexCoord1;\n" +
			"   oColor = Color;\n" +
			"}\n";

		private static string _postProcessVertexShaderSrc =
			"uniform mat4 View;\n" +
			"uniform mat4 Texm;\n" +
			"attribute vec4 Position;\n" +
			"attribute vec2 TexCoord;\n" +
			"varying  vec2 oTexCoord;\n" +
			"void main()\n" +
			"{\n" +
			"   gl_Position = View * Position;\n" +
			"   oTexCoord = vec2(Texm * vec4(TexCoord,0,1));\n" +
			"   oTexCoord.y = 1.0-oTexCoord.y;\n" +
			"}\n";

		private static string _solidFragShaderSrc = 
			"uniform vec4 Color;\n" +
			"void main()\n" +
			"{\n" +
			"   gl_FragColor = Color;\n" +
			"}\n";
		
		private static string _gouraudFragShaderSrc =
			"varying vec4 oColor;\n" +
			"void main()\n" +
			"{\n" +
			"   gl_FragColor = oColor;\n" +
			"}\n";


		private static string _textureFragShaderSrc =
			"uniform sampler2D Texture0;\n" +
			"varying vec4 oColor;\n" +
			"varying vec2 oTexCoord;\n" +
			"void main()\n" +
			"{\n" +
			"   gl_FragColor = oColor * texture2D(Texture0, oTexCoord);\n" +
			"   if (gl_FragColor.a < 0.4)\n" +
			"       discard;\n" +
			"}\n";

		private static string _alphaTextureFragShaderSrc =
			"uniform sampler2D Texture0;\n" +
			"varying vec4 oColor;\n" +
			"varying vec2 oTexCoord;\n" +
			"void main()\n" +
			"{\n" +
			"   gl_FragColor = oColor * vec4(1,1,1,texture2D(Texture0, oTexCoord).a);\n" +
			"}\n";

		private static string _postProcessFragShaderSrc =
			"uniform vec2 LensCenter;\n" +
			"uniform vec2 ScreenCenter;\n" +
			"uniform vec2 Scale;\n" +
			"uniform vec2 ScaleIn;\n" +
			"uniform vec4 HmdWarpParam;\n" +
			"uniform sampler2D Texture0;\n" +
			"varying vec2 oTexCoord;\n" +
			"\n" +
			"vec2 HmdWarp(vec2 in01)\n" +
			"{\n" +
			"   vec2  theta = (in01 - LensCenter) * ScaleIn;\n" +
			"   float rSq = theta.x * theta.x + theta.y * theta.y;\n" +
			"   vec2  theta1 = theta * (HmdWarpParam.x + HmdWarpParam.y * rSq + " +
			"                           HmdWarpParam.z * rSq * rSq + HmdWarpParam.w * rSq * rSq * rSq);\n" +
			"   return LensCenter + Scale * theta1;\n" +
			"}\n" +
			"void main()\n" +
			"{\n" +
			"   vec2 tc = HmdWarp(oTexCoord);\n" +
			"   if (!all(equal(clamp(tc, ScreenCenter-vec2(0.25,0.5), ScreenCenter+vec2(0.25,0.5)), tc)))\n" +
			"       gl_FragColor = vec4(0);\n" +
			"   else\n" +
			"       gl_FragColor = texture2D(Texture0, tc);\n" +
			"}\n";

		private static string _postProcessFullFragShaderSrc =
			"uniform vec2 LensCenter;\n" + 
			"uniform vec2 ScreenCenter;\n" +
			"uniform vec2 Scale;\n" +
			"uniform vec2 ScaleIn;\n" +
			"uniform vec4 HmdWarpParam;\n" +
			"uniform vec4 ChromAbParam;\n" +
			"uniform sampler2D Texture0;\n" +
			"varying vec2 oTexCoord;\n" +
			"\n" +
			// Scales input texture coordinates for distortion.
			// ScaleIn maps texture coordinates to Scales to ([-1, 1]), although top/bottom will be
			// larger due to aspect ratio.
			"void main()\n" +
			"{\n" +
			"   vec2  theta = (oTexCoord - LensCenter) * ScaleIn;\n" + // Scales to [-1, 1]
			"   float rSq= theta.x * theta.x + theta.y * theta.y;\n" +
			"   vec2  theta1 = theta * (HmdWarpParam.x + HmdWarpParam.y * rSq + " +
			"                  HmdWarpParam.z * rSq * rSq + HmdWarpParam.w * rSq * rSq * rSq);\n" +
			"   \n" +
			//"   // Detect whether blue texture coordinates are out of range since these will scaled out the furthest.\n"
			"   vec2 thetaBlue = theta1 * (ChromAbParam.z + ChromAbParam.w * rSq);\n" +
			"   vec2 tcBlue = LensCenter + Scale * thetaBlue;\n" +
			"   if (!all(equal(clamp(tcBlue, ScreenCenter-vec2(0.25,0.5), ScreenCenter+vec2(0.25,0.5)), tcBlue)))\n" +
			"   {\n" +
			"       gl_FragColor = vec4(0);\n" +
			"       return;\n" +
			"   }\n" +
			"   \n" +
			//"   // Now do blue texture lookup.\n"
			"   float blue = texture2D(Texture0, tcBlue).b;\n" +
			"   \n" +
			//"   // Do green lookup (no scaling).\n"
			"   vec2  tcGreen = LensCenter + Scale * theta1;\n" +
			"   vec4  center = texture2D(Texture0, tcGreen);\n" +
			"   \n" +
			//"   // Do red scale and lookup.\n"
			"   vec2  thetaRed = theta1 * (ChromAbParam.x + ChromAbParam.y * rSq);\n" +
			"   vec2  tcRed = LensCenter + Scale * thetaRed;\n" +
			"   float red = texture2D(Texture0, tcRed).r;\n" +
			"   \n" +
			"   gl_FragColor = vec4(red, center.g, blue, center.a);\n" +
			"}\n";

		private static string _lightingCommon =
			"uniform   vec3 Ambient;\n" +
			"uniform   vec4 LightPos[8];\n" +
			"uniform   vec4 LightColor[8];\n" +
			"uniform   float LightCount;\n" +
			"varying   vec4 oColor;\n" +
			"varying   vec2 oTexCoord;\n" +
			"varying   vec3 oNormal;\n" +
			"varying   vec3 oVPos;\n" +
			"vec4 DoLight()\n" +
			"{\n" +
			"   vec3 norm = normalize(oNormal);\n" +
			"   vec3 light = Ambient;\n" +
			"   for (int i = 0; i < int(LightCount); i++)\n" +
			"   {\n" +
			"       vec3 ltp = (LightPos[i].xyz - oVPos);\n" +
			"       float  ldist = length(ltp);\n" +
			"       ltp = normalize(ltp);\n" +
			"       light += clamp(LightColor[i].rgb * oColor.rgb * (dot(norm, ltp) / ldist), 0.0,1.0);\n" +
			"   }\n" +
			"   return vec4(light, oColor.a);\n" +
			"}\n";

		private static string _litSolidFragShaderSrc =
			_lightingCommon +
			"void main()\n" +
			"{\n" +
			"   gl_FragColor = DoLight() * oColor;\n" +
			"}\n";

		private static string _litTextureFragShaderSrc =
			"uniform sampler2D Texture0;\n" +
			_lightingCommon +
			"void main()\n" +
			"{\n" +
			"   gl_FragColor = DoLight() * texture2D(Texture0, oTexCoord);\n" +
			"}\n";

		private static string _multiTextureFragShaderSrc =
			"uniform sampler2D Texture0;\n" +
			"uniform sampler2D Texture1;\n" +
			"varying vec4 oColor;\n" +
			"varying vec2 oTexCoord;\n" +
			"varying vec2 oTexCoord1;\n" +
			"void main()\n" +
			"{\n" +
			"	vec4 color1 = texture2D(Texture0, oTexCoord);\n" +
			"	vec4 color2 = texture2D(Texture1, oTexCoord1);\n" +
			"	color2.rgb = color2.rgb * mix(1.9, 1.2, clamp(length(color2.rgb),0.0,1.0));\n" +
			"	color2 = color1 * color2;\n" +
			"   if (color2.a <= 0.6)\n" +
			"		discard;\n" +
			"	gl_FragColor = color2;\n" +
			"}\n";

		private string[] _vshaderSources = {
			_directVertexShaderSrc,
			_stdVertexShaderSrc,
			_postProcessVertexShaderSrc
		};

		private string[] _fshaderSources = {
			_solidFragShaderSrc,
			_gouraudFragShaderSrc,
			_textureFragShaderSrc,
			_alphaTextureFragShaderSrc,
			_postProcessFragShaderSrc,
			_postProcessFullFragShaderSrc,
			_litSolidFragShaderSrc,
			_litTextureFragShaderSrc,
			_multiTextureFragShaderSrc		
		};

		private static Dictionary<PrimitiveTypes, BeginMode> _primType2BeginModeMap = new Dictionary<PrimitiveTypes, BeginMode> {
			{ PrimitiveTypes.Lines, BeginMode.Lines },
			{ PrimitiveTypes.Points, BeginMode.Points },
			{ PrimitiveTypes.Triangles, BeginMode.Triangles },
			{ PrimitiveTypes.TriangleStrip, BeginMode.TriangleStrip },
		};

		private static Dictionary<PrimitiveTypes, PrimitiveType> _primType2PrimMap = new Dictionary<PrimitiveTypes, PrimitiveType> {
			{ PrimitiveTypes.Lines, PrimitiveType.Lines },
			{ PrimitiveTypes.Points, PrimitiveType.Points },
			{ PrimitiveTypes.Triangles, PrimitiveType.Triangles },
			{ PrimitiveTypes.TriangleStrip, PrimitiveType.TriangleStrip },
		};

		public OpenTKOSXRenderDevice (int width, int height)
		{
			GameWindow = new GameWindow () { Width = width, Height = height };

			GameWindow.Load += (sender, e) => 
			{
				GameWindow.VSync = VSyncMode.On;
				if (OnLoad != null) OnLoad();
			};
			GameWindow.RenderFrame += (sender, e) => {
				if (RenderFrame != null)
					RenderFrame ();
			};
			GameWindow.UpdateFrame += (sender, e) => {
				if (UpdateFrame != null)
					UpdateFrame ();
			};
			GameWindow.Resize += (sender, e) => {
				if (OnResize != null)
					OnResize ();
			};


			VertexShaders = new coretk.Shader[(int)BuiltinVertexShaders.Count];
			FragmentShaders = new coretk.Shader[(int)BuiltinFragmentShaders.Count];

			for (var i = 0; i < (int)BuiltinVertexShaders.Count; i++)
				VertexShaders [i] = new coretk.Shader (this, ShaderStage.Vertex, _vshaderSources [i]);
			for (var i = 0; i < (int)BuiltinFragmentShaders.Count; i++)
				FragmentShaders [i] = new coretk.Shader (this, ShaderStage.Fragment, _fshaderSources [i]);

			var gauraudShaders = new coretk.ShaderSet ();
			gauraudShaders.setShader(VertexShaders[(int)BuiltinVertexShaders.MVP]);
			gauraudShaders.setShader(FragmentShaders[(int)BuiltinFragmentShaders.Gouraud]);
			_defaultFill = new coretk.ShaderFill (gauraudShaders);
		}

		public void setLighting(LightingParams parms)
		{
			Lighting = parms;
		}

		public override void setViewport(int x, int y, int width, int height)
		{
			GL.Viewport (0, 0, width, height);
		}

		public override void clear(float r = 0.0f, float g = 0.0f, float b = 0.0f, float a = 1.0f, double depth = 1.0f)
		{
			GL.ClearColor(r, g, b, a);
			GL.ClearDepth (depth);
			GL.Clear (ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit);
		}

		public override void setDepthMode(bool enable, bool write, CompareFunc func)
		{
			if (enable) {
				GL.Enable (EnableCap.DepthTest);
				GL.DepthMask (write);
				switch (func) {
				case CompareFunc.Always:
					GL.DepthFunc (DepthFunction.Always);
					break;
				case CompareFunc.Less:
					GL.DepthFunc (DepthFunction.Less);
					break;
				case CompareFunc.Greater:
					GL.DepthFunc (DepthFunction.Greater);
					break;

				}
			} else
				GL.Disable (EnableCap.DepthTest);
		}

		public override void present()
		{
			GameWindow.SwapBuffers ();
		}

		public override void beginRendering()
		{
			GL.Enable (EnableCap.DepthTest);
			GL.Enable (EnableCap.CullFace);
			GL.FrontFace (FrontFaceDirection.Cw);

			GL.LineWidth (3.0f);
			GL.Enable (EnableCap.LineSmooth);
			GL.Enable (EnableCap.Blend);
			GL.BlendFunc (BlendingFactorSrc.SrcAlpha, BlendingFactorDest.SrcAlpha);

			GL.MatrixMode (MatrixMode.Modelview);
			GL.LoadIdentity ();
		}

		public override void setWorldUniforms (Matrix4 projection)
		{
			Projection = Matrix4.Transpose (projection);
		}

		public override void run(double frameRate)
		{
			GameWindow.Run (frameRate);
		}

		public override void render (Matrix4 matrix, Model model)
		{
			//var ms = matrix.ToString ();

			if (model.VertexBuffer == null) {
				var buffer = createBuffer () as Mono.Rift.Core.OpenTK.Buffer;
				buffer.Vertices = model.Vertices.ToArray();
				model.VertexBuffer = buffer;
			}
			if (model.IndexBuffer == null) {
				var buffer = createBuffer () as Mono.Rift.Core.OpenTK.Buffer;
				buffer.Indeces = model.Indeces.ToArray();
				model.IndexBuffer = buffer;
			}

			render ((graphics.ShaderFill)model.Fill ?? _defaultFill, model.VertexBuffer, model.IndexBuffer, matrix, 0, model.Indeces.Count, model.PrimitiveType);
		}

		public void render(graphics.ShaderFill fill, mrcoreg.Buffer vertices, mrcoreg.Buffer indeces, Matrix4 matrix, int offset, int count, PrimitiveTypes prim)
		{
			var shaders = fill.Shaders;
		
			fill.set ();
			if (shaders.ProjLoc >= 0) {
			}
			if (shaders.ViewLoc >= 0) {
			}
			if (shaders.UsesLighting && Lighting.Version != shaders.LightingVer) {
				shaders.LightingVer = Lighting.Version;
				Lighting.set (shaders);
			}

			GL.BindBuffer (BufferTarget.ArrayBuffer, ((Mono.Rift.Core.OpenTK.Buffer)vertices).GLBuffer);
			for (var i = 0; i < 5; i++) {
				GL.EnableVertexAttribArray (i);
			}

			GL.VertexAttribPointer (0, 3, VertexAttribPointerType.Float, false, 0, 0);
			GL.VertexAttribPointer (1, 4, VertexAttribPointerType.UnsignedByte, true, 0, 0);
			GL.VertexAttribPointer (2, 2, VertexAttribPointerType.Float, false, 0, 0);
			GL.VertexAttribPointer (3, 2, VertexAttribPointerType.Float, false, 0, 0);
			GL.VertexAttribPointer (4, 2, VertexAttribPointerType.Float, false, 0, 0);

			if (indeces != null) {
				GL.BindBuffer (BufferTarget.ElementArrayBuffer, ((Mono.Rift.Core.OpenTK.Buffer)indeces).GLBuffer);
				GL.DrawElements (_primType2BeginModeMap[prim], count, DrawElementsType.UnsignedShort, 0);
				GL.BindBuffer(BufferTarget.ElementArrayBuffer, 0);
			}
			else
			{
				GL.DrawArrays(_primType2PrimMap[prim], 0, count);
			}

			for (var i = 0; i < 5; i++)
			{
				GL.DisableVertexAttribArray(i);
			}
		}

		public override Mono.Rift.Core.Graphics.Buffer createBuffer()
		{
			return new Mono.Rift.Core.OpenTK.Buffer (this);
		}
	}
}

